// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
// This work is free. You can redistribute it and/or modify it
// under the terms of the WTFPL, Version 2
// For more information see LICENSE.txt or http://www.wtfpl.net/
//
// For more information, the home page:
// http://pieroxy.net/blog/pages/lz-string/testing.html
//
// LZ-based compression algorithm, version 1.4.4

const fromCharCode = String.fromCharCode;
const hasOwnProperty = Object.prototype.hasOwnProperty;

const keyStrUriSafe =
	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$";
const baseReverseDic = {};

function getBaseValue(alphabet, character) {
	if (!baseReverseDic[alphabet]) {
		baseReverseDic[alphabet] = {};
		for (let i = 0; i < alphabet.length; i++) {
			baseReverseDic[alphabet][alphabet.charAt(i)] = i;
		}
	}
	return baseReverseDic[alphabet][character];
}

//compress into uint8array (UCS-2 big endian format)
export function compressU8(uncompressed) {
	let compressed = compress(uncompressed);
	let buf = new Uint8Array(compressed.length * 2); // 2 bytes per character

	for (let i = 0, TotalLen = compressed.length; i < TotalLen; i++) {
		let current_value = compressed.charCodeAt(i);
		buf[i * 2] = current_value >>> 8;
		buf[i * 2 + 1] = current_value % 256;
	}
	return buf;
}

// Compreses with header
/**
 * @param {string} uncompressed
 * @param {number} header
 */
export function compressU8WHeader(uncompressed, header) {
	let compressed = compress(uncompressed);
	let buf = new Uint8Array(2 + compressed.length * 2); // 2 bytes per character

	buf[0] = header >>> 8;
	buf[1] = header % 256;
	for (let i = 0, TotalLen = compressed.length; i < TotalLen; i++) {
		let current_value = compressed.charCodeAt(i);
		buf[2 + i * 2] = current_value >>> 8;
		buf[2 + i * 2 + 1] = current_value % 256;
	}
	return buf;
}

//decompress from uint8array (UCS-2 big endian format)
/**
 *
 * @param {Uint8Array} compressed
 */
export function decompressU8WHeader(compressed) {
	// let buf = new Array(compressed.length / 2); // 2 bytes per character
	// for (let i = 0, TotalLen = buf.length; i < TotalLen; i++) {
	//     buf[i] = compressed[i * 2] * 256 + compressed[i * 2 + 1];
	// }

	// let result = [];
	// buf.forEach(function (c) {
	//     result.push(fromCharCode(c));
	// });
	let result = [];
	for (let i = 2, n = compressed.length; i < n; i += 2) {
		const code = compressed[i] * 256 + compressed[i + 1];
		result.push(fromCharCode(code));
	}
	return decompress(result.join(""));
}

//compress into a string that is already URI encoded
export function compressX64(input) {
	if (input == null) return "";
	return _compress(input, 6, function (a) {
		return keyStrUriSafe.charAt(a);
	});
}

//decompress from an output of compressToEncodedURIComponent
export function decompressX64(input) {
	if (input == null) return "";
	if (input == "") return null;
	input = input.replace(/ /g, "+");
	return _decompress(input.length, 32, function (index) {
		return getBaseValue(keyStrUriSafe, input.charAt(index));
	});
}

function compress(uncompressed) {
	return _compress(uncompressed, 16, function (a) {
		return fromCharCode(a);
	});
}

function _compress(uncompressed, bitsPerChar, getCharFromInt) {
	if (uncompressed == null) return "";
	let i,
		value,
		context_dictionary = {},
		context_dictionaryToCreate = {},
		context_c = "",
		context_wc = "",
		context_w = "",
		context_enlargeIn = 2, // Compensate for the first entry which should not count
		context_dictSize = 3,
		context_numBits = 2,
		context_data = [],
		context_data_val = 0,
		context_data_position = 0,
		ii;

	for (ii = 0; ii < uncompressed.length; ii += 1) {
		context_c = uncompressed.charAt(ii);
		if (!hasOwnProperty.call(context_dictionary, context_c)) {
			context_dictionary[context_c] = context_dictSize++;
			context_dictionaryToCreate[context_c] = true;
		}

		context_wc = context_w + context_c;
		if (hasOwnProperty.call(context_dictionary, context_wc)) {
			context_w = context_wc;
		} else {
			if (hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
				if (context_w.charCodeAt(0) < 256) {
					for (i = 0; i < context_numBits; i++) {
						context_data_val = context_data_val << 1;
						if (context_data_position == bitsPerChar - 1) {
							context_data_position = 0;
							context_data.push(getCharFromInt(context_data_val));
							context_data_val = 0;
						} else {
							context_data_position++;
						}
					}
					value = context_w.charCodeAt(0);
					for (i = 0; i < 8; i++) {
						context_data_val = (context_data_val << 1) | (value & 1);
						if (context_data_position == bitsPerChar - 1) {
							context_data_position = 0;
							context_data.push(getCharFromInt(context_data_val));
							context_data_val = 0;
						} else {
							context_data_position++;
						}
						value = value >> 1;
					}
				} else {
					value = 1;
					for (i = 0; i < context_numBits; i++) {
						context_data_val = (context_data_val << 1) | value;
						if (context_data_position == bitsPerChar - 1) {
							context_data_position = 0;
							context_data.push(getCharFromInt(context_data_val));
							context_data_val = 0;
						} else {
							context_data_position++;
						}
						value = 0;
					}
					value = context_w.charCodeAt(0);
					for (i = 0; i < 16; i++) {
						context_data_val = (context_data_val << 1) | (value & 1);
						if (context_data_position == bitsPerChar - 1) {
							context_data_position = 0;
							context_data.push(getCharFromInt(context_data_val));
							context_data_val = 0;
						} else {
							context_data_position++;
						}
						value = value >> 1;
					}
				}
				context_enlargeIn--;
				if (context_enlargeIn == 0) {
					context_enlargeIn = Math.pow(2, context_numBits);
					context_numBits++;
				}
				delete context_dictionaryToCreate[context_w];
			} else {
				value = context_dictionary[context_w];
				for (i = 0; i < context_numBits; i++) {
					context_data_val = (context_data_val << 1) | (value & 1);
					if (context_data_position == bitsPerChar - 1) {
						context_data_position = 0;
						context_data.push(getCharFromInt(context_data_val));
						context_data_val = 0;
					} else {
						context_data_position++;
					}
					value = value >> 1;
				}
			}
			context_enlargeIn--;
			if (context_enlargeIn == 0) {
				context_enlargeIn = Math.pow(2, context_numBits);
				context_numBits++;
			}
			// Add wc to the dictionary.
			context_dictionary[context_wc] = context_dictSize++;
			context_w = String(context_c);
		}
	}

	// Output the code for w.
	if (context_w !== "") {
		if (hasOwnProperty.call(context_dictionaryToCreate, context_w)) {
			if (context_w.charCodeAt(0) < 256) {
				for (i = 0; i < context_numBits; i++) {
					context_data_val = context_data_val << 1;
					if (context_data_position == bitsPerChar - 1) {
						context_data_position = 0;
						context_data.push(getCharFromInt(context_data_val));
						context_data_val = 0;
					} else {
						context_data_position++;
					}
				}
				value = context_w.charCodeAt(0);
				for (i = 0; i < 8; i++) {
					context_data_val = (context_data_val << 1) | (value & 1);
					if (context_data_position == bitsPerChar - 1) {
						context_data_position = 0;
						context_data.push(getCharFromInt(context_data_val));
						context_data_val = 0;
					} else {
						context_data_position++;
					}
					value = value >> 1;
				}
			} else {
				value = 1;
				for (i = 0; i < context_numBits; i++) {
					context_data_val = (context_data_val << 1) | value;
					if (context_data_position == bitsPerChar - 1) {
						context_data_position = 0;
						context_data.push(getCharFromInt(context_data_val));
						context_data_val = 0;
					} else {
						context_data_position++;
					}
					value = 0;
				}
				value = context_w.charCodeAt(0);
				for (i = 0; i < 16; i++) {
					context_data_val = (context_data_val << 1) | (value & 1);
					if (context_data_position == bitsPerChar - 1) {
						context_data_position = 0;
						context_data.push(getCharFromInt(context_data_val));
						context_data_val = 0;
					} else {
						context_data_position++;
					}
					value = value >> 1;
				}
			}
			context_enlargeIn--;
			if (context_enlargeIn == 0) {
				context_enlargeIn = Math.pow(2, context_numBits);
				context_numBits++;
			}
			delete context_dictionaryToCreate[context_w];
		} else {
			value = context_dictionary[context_w];
			for (i = 0; i < context_numBits; i++) {
				context_data_val = (context_data_val << 1) | (value & 1);
				if (context_data_position == bitsPerChar - 1) {
					context_data_position = 0;
					context_data.push(getCharFromInt(context_data_val));
					context_data_val = 0;
				} else {
					context_data_position++;
				}
				value = value >> 1;
			}
		}
		context_enlargeIn--;
		if (context_enlargeIn == 0) {
			context_enlargeIn = Math.pow(2, context_numBits);
			context_numBits++;
		}
	}

	// Mark the end of the stream
	value = 2;
	for (i = 0; i < context_numBits; i++) {
		context_data_val = (context_data_val << 1) | (value & 1);
		if (context_data_position == bitsPerChar - 1) {
			context_data_position = 0;
			context_data.push(getCharFromInt(context_data_val));
			context_data_val = 0;
		} else {
			context_data_position++;
		}
		value = value >> 1;
	}

	// Flush the last char
	// eslint-disable-next-line no-constant-condition
	while (true) {
		context_data_val = context_data_val << 1;
		if (context_data_position == bitsPerChar - 1) {
			context_data.push(getCharFromInt(context_data_val));
			break;
		} else context_data_position++;
	}
	return context_data.join("");
}

function decompress(compressed) {
	if (compressed == null) return "";
	if (compressed == "") return null;
	return _decompress(compressed.length, 32768, function (index) {
		return compressed.charCodeAt(index);
	});
}

function _decompress(length, resetValue, getNextValue) {
	let dictionary = [],
		next,
		enlargeIn = 4,
		dictSize = 4,
		numBits = 3,
		entry = "",
		result = [],
		i,
		w,
		bits,
		resb,
		maxpower,
		power,
		c,
		data = { val: getNextValue(0), position: resetValue, index: 1 };

	for (i = 0; i < 3; i += 1) {
		dictionary[i] = i;
	}

	bits = 0;
	maxpower = Math.pow(2, 2);
	power = 1;
	while (power != maxpower) {
		resb = data.val & data.position;
		data.position >>= 1;
		if (data.position == 0) {
			data.position = resetValue;
			data.val = getNextValue(data.index++);
		}
		bits |= (resb > 0 ? 1 : 0) * power;
		power <<= 1;
	}

	switch ((next = bits)) {
		case 0:
			bits = 0;
			maxpower = Math.pow(2, 8);
			power = 1;
			while (power != maxpower) {
				resb = data.val & data.position;
				data.position >>= 1;
				if (data.position == 0) {
					data.position = resetValue;
					data.val = getNextValue(data.index++);
				}
				bits |= (resb > 0 ? 1 : 0) * power;
				power <<= 1;
			}
			c = fromCharCode(bits);
			break;
		case 1:
			bits = 0;
			maxpower = Math.pow(2, 16);
			power = 1;
			while (power != maxpower) {
				resb = data.val & data.position;
				data.position >>= 1;
				if (data.position == 0) {
					data.position = resetValue;
					data.val = getNextValue(data.index++);
				}
				bits |= (resb > 0 ? 1 : 0) * power;
				power <<= 1;
			}
			c = fromCharCode(bits);
			break;
		case 2:
			return "";
	}
	dictionary[3] = c;
	w = c;
	result.push(c);

	// eslint-disable-next-line no-constant-condition
	while (true) {
		if (data.index > length) {
			return "";
		}

		bits = 0;
		maxpower = Math.pow(2, numBits);
		power = 1;
		while (power != maxpower) {
			resb = data.val & data.position;
			data.position >>= 1;
			if (data.position == 0) {
				data.position = resetValue;
				data.val = getNextValue(data.index++);
			}
			bits |= (resb > 0 ? 1 : 0) * power;
			power <<= 1;
		}

		switch ((c = bits)) {
			case 0:
				bits = 0;
				maxpower = Math.pow(2, 8);
				power = 1;
				while (power != maxpower) {
					resb = data.val & data.position;
					data.position >>= 1;
					if (data.position == 0) {
						data.position = resetValue;
						data.val = getNextValue(data.index++);
					}
					bits |= (resb > 0 ? 1 : 0) * power;
					power <<= 1;
				}

				dictionary[dictSize++] = fromCharCode(bits);
				c = dictSize - 1;
				enlargeIn--;
				break;
			case 1:
				bits = 0;
				maxpower = Math.pow(2, 16);
				power = 1;
				while (power != maxpower) {
					resb = data.val & data.position;
					data.position >>= 1;
					if (data.position == 0) {
						data.position = resetValue;
						data.val = getNextValue(data.index++);
					}
					bits |= (resb > 0 ? 1 : 0) * power;
					power <<= 1;
				}
				dictionary[dictSize++] = fromCharCode(bits);
				c = dictSize - 1;
				enlargeIn--;
				break;
			case 2:
				return result.join("");
		}

		if (enlargeIn == 0) {
			enlargeIn = Math.pow(2, numBits);
			numBits++;
		}

		if (dictionary[c]) {
			// @ts-ignore
			entry = dictionary[c];
		} else {
			if (c === dictSize) {
				entry = w + w.charAt(0);
			} else {
				return null;
			}
		}
		result.push(entry);

		// Add w+entry[0] to the dictionary.
		dictionary[dictSize++] = w + entry.charAt(0);
		enlargeIn--;

		w = entry;

		if (enlargeIn == 0) {
			enlargeIn = Math.pow(2, numBits);
			numBits++;
		}
	}
}
